package ai;

import de.szut.pongsim.ai.User;
import de.szut.pongsim.physics.PadMovement;
import de.szut.pongsim.physics.Point;

/**
 * Basic pong-AI that predicts
 * the impact position of the ball
 * and tries to be a bit unpredictable.
 * @author Marc Huisinga
 *
 */
public class AI_Marc implements User {
	
	private static final int HEIGHT = 60;
	private static final int MIDDLE_Y = HEIGHT/2;
	private static final int COLLISION_TOP = HEIGHT-1;
	private static final int COLLISION_BOTTOM = 0;
	private static final int COLLISION_LEFT = 1;
	private static final int COLLISION_RIGHT = 63;
	
	private boolean isLeft = false;
	
	private double m;
	private Point collision;
	private Point current;
	private int direction;
	private boolean collided;
	private double finalVerticalDir;
	
	@Override
	public void reset() {
		m = 0;
		collision = null;
		current = null;
		direction = 0;
		collided = false;
		finalVerticalDir = 0;
	}
	
	public AI_Marc() {
		reset();
	}

	/**
	 * Determines if the ball is moving right
	 * @return moving right
	 */
	private boolean movingRight() {
		return direction > 0;
	}
	
	/**
	 * Determines if the ball is moving left
	 * @return moving left
	 */
	private boolean movingLeft() {
		return direction < 0;
	}
	
	/**
	 * Determines how many steps are left
	 * until the left or the right end
	 * of the field.
	 * @param x - ball position
	 * @return steps
	 */
	private int leftoverSteps(int x) {
		if (movingRight()) {
			return COLLISION_RIGHT - x;
		}
		if (movingLeft()) {
			return COLLISION_LEFT - x;
		}
		return x;
	}
	
	/**
	 * Determines how many upper and lower collisions
	 * a ball will have before reaching the left
	 * or the right end of the field.
	 * @param y - ball position
	 * @param totalY - end y-coordinate of the ball
	 * @return amount of collisions
	 */
	private int collisions(int y, int totalY) {
		// m is inverted for left movement
		boolean movingUpwards = m >= 0 && movingRight() || m < 0 && movingLeft();
		if (movingUpwards) {
			return totalY / HEIGHT;
		}
		int totalLowerY =  totalY - COLLISION_TOP;
		return totalLowerY / HEIGHT;
	}
	
	/**
	 * Predicts the y-position
	 * at which the ball is likely
	 * to impact at the left or the right
	 * end of the field.
	 * @return impact y
	 */
	private int predictFinish() {
		int x = current.getX();
		int y = current.getY();
		int steps = leftoverSteps(x);
		int movedY = (int) (steps*m);
		int totalY = y + movedY;
		int endY = totalY % HEIGHT;
		// Assures, for HEIGHT == 60, that 
		//   endY == -1 -> 59,
		//   endY == -2 -> 58,
		//   etc.
		// for the function to start from the top
		// after underflowing from the bottom
		if (endY < COLLISION_BOTTOM) {
			endY += HEIGHT;
		}
		int collisions = collisions(y, totalY);
		finalVerticalDir = direction*m*Math.pow(-1, collisions)/Math.abs(m);
		if (collisions % 2 != 0) {
			endY = COLLISION_TOP - endY;
		}
		return endY;
	}
	
	/**
	 * Determines the optimal target section
	 * of the pad with the most randomness
	 * and the least required movement.
	 * @param pad - y
	 * @return target section y
	 */
	private int padTarget(int pad) {
		boolean movingUpwards = finalVerticalDir > 0;
		if (movingUpwards) {
			int safePadBottom = pad + 1;
			return safePadBottom;
		}
		int safePadTop = pad + 4;
		return safePadTop;
	}
	
	/**
	 * Determines a move for the pad towards the goal.
	 * @param pad
	 * @param goal
	 * @return movement
	 */
	private static PadMovement movementTowards(int pad, int goal) {
		if (pad < goal) {
			return PadMovement.UP;
		}
		if (pad > goal) {
			return PadMovement.DOWN;
		}
		return PadMovement.STOP;
	}
	
	@Override
	public PadMovement nextStep(int ownPadBottomY, int enemyPadBottomY) {
		if (direction == 0) {
			return PadMovement.STOP;
		}
		boolean isDefender = movingRight() && !isLeft || movingLeft() && isLeft;
		if (isDefender) {
			int predictedY = predictFinish();
			return movementTowards(padTarget(ownPadBottomY), predictedY);
		}
		int padMiddle = ownPadBottomY + 2;
		return movementTowards(padMiddle, MIDDLE_Y);
	}
	
	/**
	 * Determines if the ball at the current
	 * position collided with an upper or lower
	 * side.
	 * @return collided
	 */
	private boolean collidedSide() {
		int y = current.getY();
		if (y == COLLISION_BOTTOM || y == COLLISION_TOP) {
			// Hack to not get stuck in
			// a collision loop in case of
			// y being at a collision point
			// twice in a row
			if (!collided) {
				collided = true;
				return true;
			}
			return false;
		}
		collided = false;
		return false;
	}
	
	@Override
	public void updateBallPos(Point ball) {
		// First update after round start
		if (collision == null) {
			collision = ball;
			current = ball;
			return;
		}
		current = ball;
		int currentX = current.getX();
		int collisionX = collision.getX();
		int deltaX = currentX - collisionX;
		int currentY = current.getY();
		int collisionY = collision.getY();
		m = ((double) (currentY - collisionY))/(deltaX);
		int currentDirection = deltaX/Math.abs(deltaX);
		// Assures that direction is only
		// used as initializing direction after
		// round start
		if (currentDirection != 0) {
			direction = currentDirection;
		}
		boolean collidedPad = currentX == COLLISION_LEFT || currentX == COLLISION_RIGHT;
		if (collidedPad) {
			m = -m;
			collision = current;
			direction = -direction;
			collided = false;
		} else if (collidedSide()) {
			m = -m;
			// Moves the point of collision
			// to a point on the new path
			collision = new Point(collisionX, 2*currentY-collisionY);
		}
	}

	@Override
	public void setLeftSide() {
		isLeft = true;
	}

	@Override
	public void setRightSide() {
		isLeft = false;
	}
	
}
